package com.dyz.wsw.passport.service.impl;

import com.dyz.wsw.common.consts.web.HttpConst;
import com.dyz.wsw.common.consts.web.JwtConst;
import com.dyz.wsw.common.enumerator.ServiceCode;
import com.dyz.wsw.common.ex.ServiceException;
import com.dyz.wsw.common.pojo.auth.Principal;
import com.dyz.wsw.common.pojo.cache.UserCachePO;
import com.dyz.wsw.common.utils.converter.Json;
import com.dyz.wsw.common.utils.security.AES;
import com.dyz.wsw.common.utils.security.JWTs;
import com.dyz.wsw.common.utils.security.MD5;
import com.dyz.wsw.common.utils.security.RSA;
import com.dyz.wsw.common.web.JsonResult;
import com.dyz.wsw.passport.dao.cache.IUserCacheRepository;
import com.dyz.wsw.passport.dao.persist.repository.IUserRepository;
import com.dyz.wsw.passport.pojo.entity.LoginLog;
import com.dyz.wsw.passport.pojo.entity.User;
import com.dyz.wsw.passport.pojo.param.UserLoginInfoParam;
import com.dyz.wsw.passport.pojo.vo.UserLoginInfoVO;
import com.dyz.wsw.passport.pojo.vo.UserLoginResultVO;
import com.dyz.wsw.passport.service.IUserService;
import com.dyz.wsw.passport.utils.IPUtils;
import com.dyz.wsw.security.config.WswSecurityJWTProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.data.relational.core.query.Criteria;
import org.springframework.data.relational.core.query.Query;
import org.springframework.data.relational.core.query.Update;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author dyz
 * @date 2025/2/8 10:47
 */
@Service
@Slf4j
public class IUserServiceImpl implements IUserService, JwtConst, HttpConst {


    private final PasswordEncoder passwordEncoder;
    private final WswSecurityJWTProperties properties;
    private final IUserRepository iUserRepository;
    private final R2dbcEntityTemplate template;
    private final IUserCacheRepository iUserCacheRepository;

    public IUserServiceImpl(
            PasswordEncoder passwordEncoder,
            WswSecurityJWTProperties properties,
            IUserRepository iUserRepository,
            R2dbcEntityTemplate template,
            IUserCacheRepository iUserCacheRepository
    ) {
        log.info("创建登录服务对象: IUserServiceImpl");
        this.passwordEncoder = passwordEncoder;
        this.properties = properties;
        this.iUserRepository = iUserRepository;
        this.template = template;
        this.iUserCacheRepository = iUserCacheRepository;
    }

    @Override
    public Mono<JsonResult> doLogin(UserLoginInfoParam loginInfo, ServerWebExchange exchange) {
        log.debug("开始处理【根据表单处理登录业务】, loginInfo={}", loginInfo);
        String username = loginInfo.getUsername();
        String password = loginInfo.getPassword();
        String key = loginInfo.getKey();

        return handleLogin(username,password,key)
                .flatMap(userLoginInfoVO -> {
                    String userAgent = exchange.getRequest().getHeaders().getFirst(HEADER_USER_AGENT);
                    String ip = IPUtils.getIp(exchange.getRequest());

                    return loginSuccess(userLoginInfoVO,userAgent,ip)
                            .map(JsonResult::ok);
                })
                // 用户名不存在
                .defaultIfEmpty(JsonResult.fail(ServiceCode.ERROR_UNAUTHORIZED,"用户名或密码错误!"));
    }

    @Override
    public Mono<JsonResult> doLogout(Principal principal) {
        log.debug("开始处理【退出登录】的业务, 参数: {}", principal);
        Long id = principal.getId();

        return iUserCacheRepository.deleteUserState(id)
                .handle((count,sink)->{
                    if(count!=1){
                        String message = "操作失败，用户数据有误!";
                        log.warn(message);
                        sink.error(new ServiceException(ServiceCode.ERROR_NOT_FOUND,message));
                        return;
                    }
                    sink.next(JsonResult.ok("登出成功"));
                    sink.complete();
                });
    }

    /**
     * 登录成功后处理逻辑
     * 1.记录一条登录日志
     * 2.更新登录次数
     * 3.生成jwt
     * 4.将用户状态,权限等必要信息存入redis中
     * @param userLoginInfoVO 用户登录信息
     * @param userAgent 浏览器信息
     * @param ip ip地址
     * @return 用户登录结果VO
     */
    private Mono<UserLoginResultVO> loginSuccess(UserLoginInfoVO userLoginInfoVO,String userAgent,String ip){
        Long userId = userLoginInfoVO.getId();
        Integer loginCount = userLoginInfoVO.getLoginCount();
        String avatar = userLoginInfoVO.getAvatar();
        String username = userLoginInfoVO.getUsername();

        List<String> permissions = userLoginInfoVO.getPermissions();
        String permissionJsonString = Json.toJson(permissions);

        LocalDateTime now = LocalDateTime.now();
        LoginLog loginLog = new LoginLog();
        loginLog.setUserId(userId);
        loginLog.setUsername(username);
        loginLog.setIp(ip);
        loginLog.setUserAgent(userAgent);
        loginLog.setGmtLogin(now);


        return insertLoginLog(loginLog)
                .flatMap(resultBool->{
                    if(!resultBool){
                        log.warn("登录日志插入失败，即将返回！");
                        return Mono.error(new ServiceException(ServiceCode.ERROR_UPDATE,"登录失败，服务器忙，请稍后在尝试！"));
                    }
                    return template.update(User.class)
                            .matching(Query.query(Criteria.where("id").is(userId)))
                            .apply(Update.update("login_count",loginCount+1))
                            .onErrorReturn(-1L);
                })
                .flatMap((rows)->{
                    if (rows != 1){
                        log.warn("登录次数+1失败，即将返回！");
                        return Mono.error(new ServiceException(ServiceCode.ERROR_UPDATE,"登录失败，服务器忙，请稍后在尝试！"));
                    }
                    return issueToken(userLoginInfoVO,userAgent,ip);
                })
                .flatMap(token->{
                    UserCachePO userCachePO = new UserCachePO(ENABLE_STATE_ON, permissionJsonString);
                    log.debug("像缓存中存入用户状态数据: {}",userCachePO);
                    return iUserCacheRepository.saveUserState(userId,userCachePO)
                            .map(saveResult->{
                                UserLoginResultVO userLoginResultVO = new UserLoginResultVO()
                                        .setId(userId)
                                        .setUsername(username)
                                        .setAvatar(avatar)
                                        .setToken(token)
                                        .setAuthorities(permissions);
                                log.debug("即将返回用户的登录结果: {}",userLoginResultVO);
                                return userLoginResultVO;
                            });
                });


    }

    /**
     * 颁发令牌
     * //todo 后期可以考虑在payload中添加一条生成时间戳，然后校验时根据时间戳差值来额外的定义jwt最长有效期
     * //todo 前端也可以解析jwt辅助校验，若过期了，就不发请求了
     * @return jwt令牌
     */
    private Mono<String> issueToken(UserLoginInfoVO userLoginInfoVO,String userAgent,String ip){
        //颁发步骤
        Map<String,Object> payload = new HashMap<>();
        payload.put(CLAIM_JWT_ISS,"dyz");
        payload.put(CLAIM_USER_ID,userLoginInfoVO.getId());
        payload.put(CLAIM_USER_NAME,userLoginInfoVO.getUsername());
        // 将ip和 userAgent作为特征存储jwt，校验时也是先算出ip+userAgent的md5或者考虑uuid，来检验是否存在盗用问题
        // 暂时只用ip即可
        payload.put(CLAIM_JWT_JTI, MD5.encrypt(ip));
        payload.put(CLAIM_JWT_IAT,System.currentTimeMillis());
        //也可以单独为这个jwt准备一份钥匙,这里就用密码加解密的私钥
        //如果单独准备了,还注意一起将校验时用到的私钥引用也修改了
        String jwt = JWTs.generate(payload, properties.getPrivateKey());
        log.debug("生成用户的jwt数据：{}",jwt);
        return Mono.just(jwt);
    }


    /**
     * 插入登录日志记录
     * @param loginLog 登录日志对象
     * @return 插入结果(true or false)
     */
    public Mono<Boolean> insertLoginLog(LoginLog loginLog){
        return template.insert(loginLog)
                .flatMap(result->{
                    if(result!=null&&result.getId()!=null){
                        return Mono.just(true);
                    }else {
                        return Mono.just(false);
                    }
                })
                .onErrorReturn(false);
    }



    /**
     * 登录处理
     * @param username 用户名
     * @param password 密码
     * @param key 密钥
     * @return 处理结果
     */
    private Mono<UserLoginInfoVO> handleLogin(String username, String password, String key){
        return iUserRepository.getUserLoginInfoByUsername(username)
                .filter(userLoginInfoVO -> userLoginInfoVO.getId()!=null)
                .handle((userLoginInfoVO, sink)-> {
                    log.debug("userLoginInfoVO={}", userLoginInfoVO);
                    String rawPassword = getRawPassword(key, password);
                    log.debug("rawPassword={}", rawPassword);
                    boolean matches = passwordEncoder.matches(rawPassword, userLoginInfoVO.getPassword());
                    if (!matches) {
                        log.warn("user: {},密码错误",userLoginInfoVO.getUsername());
                        sink.error(new ServiceException(ServiceCode.ERROR_UNAUTHORIZED,"用户名或密码错误!"));
                        return;
                    }
                    if (userLoginInfoVO.getEnable()==0){
                        log.warn("user: {}被封禁,请联系管理员!",userLoginInfoVO.getUsername());
                        sink.error(new ServiceException(ServiceCode.ERROR_UNAUTHORIZED_DISABLED,"此用户被封禁!"));
                        return;
                    }
                    sink.next(userLoginInfoVO);
                    sink.complete();
                });
    }

    /**
     * 尝试还原密码
     * @param key 加密后的AES密钥
     * @param encryptedPassword 加密密码
     * @return 原始密码
     */
    private String getRawPassword(String key,String encryptedPassword){
        String rawPassword;
        try {
            String aesKey = RSA.decrypt(key, properties.getPrivateKey());
            rawPassword = AES.decrypt(encryptedPassword, aesKey);
        } catch (Exception e) {
            throw new ServiceException(ServiceCode.ERROR_ILLEGAL_PASSWORD,"Illegal password");
        }
        return rawPassword;
    }



}
